﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using BitButterCORE.Common;
using BitButterCORE.Component;
using BitButterCORE.Event;
using BitButterCORE.Object;

namespace BitButterCORE.Rule
{
	public abstract class BaseRule<TRule> : BaseSingleton<TRule>, IRule
	{
		public void ApplyToObject(IObjectReference<BaseObject> appliedObject)
		{
			if (appliedObject.Object == null)
			{
				return;
			}

			if (!AppliedObjects.ContainsKey(appliedObject))
			{
				var applierMethods = MatchApplierAction(appliedObject);
				AppliedObjects.Add(appliedObject, applierMethods);
			}
		}

		Dictionary<IObjectReference<BaseObject>, Dictionary<Type, MethodInfo>> AppliedObjects => appliedObjects ?? (appliedObjects = new Dictionary<IObjectReference<BaseObject>, Dictionary<Type, MethodInfo>>());
		Dictionary<IObjectReference<BaseObject>, Dictionary<Type, MethodInfo>> appliedObjects;

		Dictionary<Type, MethodInfo> MatchApplierAction(IObjectReference<BaseObject> appliedObject)
		{
			var result = new Dictionary<Type, MethodInfo>();

			if (appliedObject.Object != null)
			{
				var applierMethods = GetType()
									.GetMethods(BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)
									.Where(method =>
									{
										var firstParameter = method.GetParameters().FirstOrDefault();
										var secondParameter = method.GetParameters().Skip(1).FirstOrDefault();
										return firstParameter != null
											&& secondParameter != null
											&& typeof(IObjectReference<BaseObject>).IsAssignableFrom(firstParameter.ParameterType)
											&& typeof(BaseEventArgs).IsAssignableFrom(secondParameter.ParameterType);
									});
				var applierMethodsGroupedByEventArgs = applierMethods.GroupBy(
									method => method.GetParameters().Skip(1).First().ParameterType,
									(eventArgsType, methods) => new Tuple<Type, MethodInfo>(
										eventArgsType,
										methods.OrderByDescending(method => method.GetParameters()
											.Where(parameter => typeof(BaseComponent).IsAssignableFrom(parameter.ParameterType))
											.Count()).First())
									);

				foreach (var (eventArgsType, applierMethod) in applierMethodsGroupedByEventArgs)
				{
					result.Add(eventArgsType, applierMethod);
				}
			}

			return result;
		}

		protected void RegisterAsEventModifier<TEventArg, TEvent>(TEvent eventType)
			where TEventArg : BaseModifiableEventArgs
			where TEvent : BaseModifiableEvent<TEventArg, TEvent>
		{
			eventType.Modifier += Apply;
		}

		protected void UnregisterEventModifier<TEventArg, TEvent>(TEvent eventType)
			where TEventArg : BaseModifiableEventArgs
			where TEvent : BaseModifiableEvent<TEventArg, TEvent>
		{
			eventType.Modifier -= Apply;
		}

		protected void RegisterAsEventHandler<TEventArg, TEvent>(TEvent eventType)
			where TEventArg : BaseEventArgs
			where TEvent : BaseEvent<TEventArg, TEvent>
		{
			eventType.Handler += Apply;
		}

		protected void UnregisterEventHandler<TEventArg, TEvent>(TEvent eventType)
			where TEventArg : BaseEventArgs
			where TEvent : BaseEvent<TEventArg, TEvent>
		{
			eventType.Handler -= Apply;
		}

		protected virtual void Apply(IObjectReference<BaseObject> sender, BaseEventArgs eventArgs)
		{
			if (RuleIsAppliedToObject(sender))
			{
				var (matchedMethod, parameters) = GetApplierMethodAndParameters(sender, eventArgs);
				if (matchedMethod != null && parameters != null)
				{
					matchedMethod.Invoke(this, parameters);
				}
			}
		}

		protected bool RuleIsAppliedToObject(IObjectReference<BaseObject> sender)
		{
			return sender != null && sender.Object != null && AppliedObjects.ContainsKey(sender);
		}

		protected Tuple<MethodInfo, object[]> GetApplierMethodAndParameters(IObjectReference<BaseObject> sender, BaseEventArgs eventArgs)
		{
			var appliedObject = sender.Object;
			var appliedMethodsForObject = AppliedObjects[sender];
			if (appliedMethodsForObject.ContainsKey(eventArgs.GetType()))
			{
				var matchedMethodForEvent = appliedMethodsForObject[eventArgs.GetType()];
				var componentTypes = matchedMethodForEvent.GetParameters()
					.Where(x => typeof(BaseComponent).IsAssignableFrom(x.ParameterType))
					.Select(x => x.ParameterType);
				var components = componentTypes.Select(x =>
				{
					var method = appliedObject.GetType().GetMethod("GetComponent").MakeGenericMethod(new[] { x });
					return method.Invoke(appliedObject, null);
				}).Where(x => x != null).ToArray();

				if (components.Length == componentTypes.Count())
				{
					var parameters = new object[] { sender, eventArgs }.Concat(components).ToArray();
					return new Tuple<MethodInfo, object[]>(matchedMethodForEvent, parameters);
				}
			}
			return new Tuple<MethodInfo, object[]>(null, null);
		}
	}
}
